/**********************************************************************\
 © COPYRIGHT 2006 Corporation for National Research Initiatives (CNRI);
   All rights reserved.

 The HANDLE.NET software is made available subject to the
 Handle System Public License, which may be obtained at 
 http://hdl.handle.net/4263537/5030 or hdl:4263537/5030
\**********************************************************************/

package net.handle.apps.servlet_proxy;

import java.io.*;
import java.util.*;
import java.text.*;
import java.net.InetAddress;

import net.handle.hdllib.*;
import net.handle.util.*;

/*******************************************************************************
* Log class that writes error and log information to files that rotate on a 
* monthly or daily basis.
******************************************************************************/

public class RotatingAccessLog
  implements Runnable
{
  public static final int ACCESS_LOG_BUFFER_SIZE = 100000;
  public static final int ERRLOG_LEVEL_EVERYTHING =   0;   // == log all messages
  public static final int ERRLOG_LEVEL_INFO       =  25;
  public static final int ERRLOG_LEVEL_NORMAL     =  50;
  public static final int ERRLOG_LEVEL_REALBAD    =  75;
  public static final int ERRLOG_LEVEL_FATAL      = 100;
  
  public static final int ROTATE_MONTHLY = 0;
  public static final int ROTATE_DAILY = 0;
  
  private int errorLoggingLevel = ERRLOG_LEVEL_INFO;     // Default logging level
    
  private File logDirectory = null;
  
  private Writer accessWriter = null;
  private Writer errorWriter = null;
  private DateFormat errorLogDateFormat = null;
  
  private boolean continuing      = true; // Signal to keep going...or not
  private boolean loggingAccesses = true;
  
  private final String ERROR_LOG_LOCK  = "error_log_lock";
  private final String ACCESS_LOG_LOCK = "access_log_lock";
  
  private Thread flusherThread = null;
  private Thread rotaterThread = null;

  private static Object monthlyLock = new Object();
  private static RotatingAccessLog monthlyLogger = null;

  /** 
    * Return the singleton monthly RotatingAccessLog object.  
    * If the monthly RotatingAccessLog singleton does not already 
    * exist, construct one with the given configuration settings.
    */
  public static final RotatingAccessLog getMonthlyLogger(File logDir) 
    throws Exception
  {
    if(monthlyLogger!=null) return monthlyLogger;
    
    synchronized (monthlyLock) {
      if(monthlyLogger==null) {
        System.gc();
        System.runFinalization();
        monthlyLogger = new RotatingAccessLog(logDir, true);
      }
    }
    return monthlyLogger;
  }
  
  
  /** 
    * Construct a log handler to write to log files under the specified directory.
    * If the directory doesnt exist, isnt writable, or is null, the handler
    * will just write messages to stderr.
    */
  public RotatingAccessLog(File logDir, boolean monthly)
    throws Exception
  {
    if(logDir==null) throw new NullPointerException("Log directory cannot be null");
    
    this.logDirectory = logDir;
    this.errorLogDateFormat = new SimpleDateFormat("yyyy/MM/dd hh:mm:ss z");
      
    LogRotater logRotater = null;
    if(monthly) {
      logRotater = new MonthlyRotater();
    } else {
      logRotater = new DailyRotater();
    }
    
    // make sure the log directory exists and is a directory
    if (logDirectory.exists()) {
      if (!logDirectory.isDirectory())
        throw new Exception("\"" + logDirectory.getAbsolutePath() + "\" is not a directory.");
    } else {
      logDirectory.mkdirs();
    }
    
    // allow the rotater to initialize itself now that all the settings are loaded
    logRotater.init();
      
    // kick off the rotater
    Thread rotaterThread = new Thread(logRotater);
    rotaterThread.setPriority(Thread.MIN_PRIORITY);
    rotaterThread.setDaemon(true);
    rotaterThread.start();
    
    // wait for the log rotater to get started
    while(!logRotater.initialized()) {
      try { Thread.sleep(500); } catch (Throwable t) {}
    }
    
    logError(ERRLOG_LEVEL_INFO, "Started new run.");
    
    flusherThread = new Thread(this);
    flusherThread.setDaemon(true);
    flusherThread.start();
  }

  
  /** 
    * Sets the level at which messages equal to or over will be logged.
    */
  public void setErrorLogLevel(int newLogLevel) {
    errorLoggingLevel = newLogLevel;
  }

  
  
  public void logAccess(String logStr) {
    synchronized(ACCESS_LOG_LOCK) {
      if (accessWriter == null) {
        System.err.println(logStr);
      } else {
        try {
          accessWriter.write(logStr);
          accessWriter.write('\n');
        } catch (Exception e) {
          System.err.println("Error writing to access log: (" + e + "): " + logStr);
        }
      }
    }
  }
  
  /** 
    * Write a message to the access log
    */
  public void logAccess(String accessType, InetAddress clientAddr,
                        int opCode, int rsCode, String logString, long time)
  {
    if (logString != null) {
      String msg =   ((clientAddr == null) ? "" : clientAddr.getHostAddress())
      + " " + accessType + " \"" + (new Date()) + "\" "
      + opCode + " " + rsCode + " " + time + "ms " + logString;
      logAccess(msg);
    }
  }

  
  /** 
    * Write a message to the error log
    */
  public void logError(int level, String logString) {
    if (level < errorLoggingLevel || logString == null)    // No-op in either case
      return;
    
    String msg = "\"" + errorLogDateFormat.format(new Date()) + "\" " + level + ' ' + logString;
    
    // If level is "fatal", write to stderr
    // even if writing to the error log, too
    
    if ((level == ERRLOG_LEVEL_FATAL) || (errorWriter == null))
      System.err.println(msg);
    
    if (errorWriter != null) {
      synchronized(ERROR_LOG_LOCK) {
        try {
          errorWriter.write(msg + '\n');
          errorWriter.flush();
        } catch (Throwable e) {
          System.err.println("Error (" + e + ") writing \"" + logString + "\" to error log.");
        }
      }
    }
  }
  
  /**
    * Sets the file where access log entries will be written.  If the file already exists
   * then new entries will be appended to the file.
   */
  private void setAccessLogFile(File newAccessLogFile)
    throws IOException
  {
    synchronized(ACCESS_LOG_LOCK) {
      // close the old access log
      if(accessWriter!=null) {
        accessWriter.flush();
        accessWriter.close();
        accessWriter = null;
      }
      
      if(loggingAccesses) {
        accessWriter = new BufferedWriter(new FileWriter(newAccessLogFile.getAbsolutePath(), true),
                                          ACCESS_LOG_BUFFER_SIZE);
      }
    }
  }
  
  /*
   * Sets the file where error log entries will be written.  If the file already exists
   * then new entries will be appended to the file.
   */    
  private void setErrorLogFile(File newErrorLogFile)
    throws IOException
  {
    synchronized(ERROR_LOG_LOCK) {
      // close the old error log
      Writer oldWriter = null;
      try {
        oldWriter = errorWriter;
        errorWriter = null;
        
        FileOutputStream errf = new FileOutputStream(newErrorLogFile.getAbsolutePath(), true);
        System.setErr(new PrintStream(errf));         // Redirect stderr to the error log
        errorWriter = new OutputStreamWriter(errf);
      } finally {
        try {
          if(oldWriter!=null)
            oldWriter.close();
        } catch (Throwable t) {}
      }
    }
  }
  
  
  /**
    * The run() implementation for the flusher thread:
   * Flush the Access-Log output about every 60 seconds (asynchronously, to
                                                         * minimize the impact on server speed).  (Error-log output is flushed with
                                                                                                   * each write.)
   */
  public void run() {
    while (continuing) {
      try {
        synchronized(ACCESS_LOG_LOCK) {
          if (accessWriter != null)
            accessWriter.flush();
        }
      } catch (Exception e) { /* Ignore */ }
      
      try {
        Thread.currentThread().sleep(60000);
      } catch (Exception e) { /* Ignore */ }
    }
  }
  
  
  /**
    * Stop the flusher thread and close the logs.
   */
  public void shutdown() {
    continuing = false;                             // Tell flusher thread to quit
    if (flusherThread != null) {
      synchronized(ACCESS_LOG_LOCK) {                   // Wake the flusher thread, if it's asleep
        try { flusherThread.interrupt(); } catch (Exception e) { /* Ignore */ }
        try { rotaterThread.interrupt(); } catch (Exception e) { /* Ignore */ }
      }
    }
    
    if (accessWriter != null) {
      try { accessWriter.close(); } catch (Exception e) { /* Ignore */ }
    }
    
    if (errorWriter != null) {
      try { errorWriter.close(); } catch (Exception e) { /* Ignore */ }
    }
  }
  
  
  private abstract class LogRotater
    implements Runnable
  {
    private boolean isInitialized = false;
    
    /**
    * Initialize the log rotater
     */
    public void init() {}
    
    /**
      * Return the next time that the logs should be rotated in milliseconds
     */
    public abstract long getNextRotationTime(long currentTime);
    
    /**
      * Return the suffix that should be appended to access/error log file names
     * for the time period containing the given time.
     */
    public abstract String getLogFileSuffix(long currentTime);
    
    public boolean initialized() {
      return isInitialized;
    }
    
    public void run() {
      while(continuing) {
        long now = System.currentTimeMillis();
        
        try {
          // get the name of the log file for the current period
          String logFileSuffix = getLogFileSuffix(now);
          setAccessLogFile(new File(logDirectory, HSG.ACCESS_LOG_FILE_NAME_BASE + logFileSuffix));
          setErrorLogFile(new File(logDirectory, HSG.ERROR_LOG_FILE_NAME_BASE + logFileSuffix));
        } catch (Throwable t) {
          System.err.println("Error setting log files: "+t);
          t.printStackTrace(System.err);
        }
        isInitialized = true;
        
        // wait until the next rotation time
        long nextRotationTime = getNextRotationTime(now);
        while(nextRotationTime > System.currentTimeMillis()) {
          try {
            Thread.sleep(Math.max(0, nextRotationTime - System.currentTimeMillis()));
          } catch (Throwable t) { }
        }
      }
    }
    
    /**
      * Return a date-stamp for the given date.
     */
    protected String getSuffixForDate(Calendar cal) {
      return "-"+String.valueOf(cal.get(Calendar.YEAR)*10000 +
                                (cal.get(Calendar.MONTH)+1)*100 +
                                cal.get(Calendar.DAY_OF_MONTH));
    }
  }
  
  
  class MonthlyRotater
    extends LogRotater
  {
    protected Calendar cal = Calendar.getInstance();
    
    /**
    * Return the next time that the logs should be rotated in milliseconds
     */
    public long getNextRotationTime(long currentTime) {
      cal.setTime(new Date(currentTime));
      int thisMonth = cal.get(Calendar.MONTH);
      while(cal.get(Calendar.MONTH)==thisMonth)
        cal.add(Calendar.DATE, 1);
      cal.set(Calendar.HOUR, 0);
      cal.set(Calendar.MINUTE, 0);
      cal.set(Calendar.SECOND, 1);
      cal.set(Calendar.MILLISECOND, 0);
      return cal.getTime().getTime();
    }
    
    /**
      * Return the suffix that should be appended to access/error log file names
     * for the time period containing the given time.
     */
    public String getLogFileSuffix(long currentTime) {
      cal.setTime(new Date(currentTime));
      while(cal.get(Calendar.DAY_OF_MONTH)!=1)
        cal.add(Calendar.DATE, -1);
      cal.set(Calendar.HOUR, 0);
      cal.set(Calendar.MINUTE, 0);
      cal.set(Calendar.SECOND, 1);
      
      return "-"+String.valueOf(cal.get(Calendar.YEAR)*100 +
                                (cal.get(Calendar.MONTH)+1));
    }
  }
  
  
  class DailyRotater
    extends LogRotater
  {
    protected Calendar cal = Calendar.getInstance();
    
    /**
    * Return the next time that the logs should be rotated in milliseconds
     */
    public long getNextRotationTime(long currentTime) {
      cal.setTime(new Date(currentTime));
      cal.add(Calendar.DATE, 1);
      cal.set(Calendar.HOUR, 0);
      cal.set(Calendar.MINUTE, 0);
      cal.set(Calendar.SECOND, 1);
      return cal.getTime().getTime();
    }
    
    /**
      * Return the suffix that should be appended to access/error log file names
     * for the time period containing the given time.
     */
    public String getLogFileSuffix(long currentTime) {
      cal.setTime(new Date(currentTime));
      cal.set(Calendar.HOUR, 0);
      cal.set(Calendar.MINUTE, 0);
      cal.set(Calendar.SECOND, 1);
      return getSuffixForDate(cal);
    }
  }
  
  class DefaultRotater
    extends LogRotater
  {
    protected Calendar cal = Calendar.getInstance();
    
    /**
    * Return the next time that the logs should be rotated in milliseconds
     */
    public long getNextRotationTime(long currentTime) {
      return Long.MAX_VALUE;
    }
    
    /**
      * Return the suffix that should be appended to access/error log file names
     * for the time period containing the given time.
     */
    public String getLogFileSuffix(long currentTime) {
      return "";
    }
  }
  
}
